React + Redux + TypeScript 实践二:基于 axios 的 services

本文在实践一的基础上,通过例子讲述如何根据 axios 库构建前端需要的请求函数。

前后端交互的基础架构如图(window 会监听响应出错事件):

http.png

  • 响应数据格式

将后端返回的响应数据格式简单地设定为:

1
2
3
4
5
{
"code": 0,
"error": "错误信息",
"data": "响应数据"
}

响应字段含义如下表:

name type description
code number 响应码, 0 代表正常,1 代表异常
error string / null 响应错误信息,code=0 时为 null
data any 响应数据,code=1 时为 null

定义了响应数据格式后,可以通过 TypeScript 对其进一步抽象:

1
2
3
4
5
6
7
// 文件:client/types/http-api.d.ts
// 后端响应数据接口
interface IResponse<T> {
code: number
error: string | null
data: T
}

由于不同的后端接口返回的响应 data 数据格式不一致,因此需要开发者在使用 IResponse 的时候具体指定 T

  • 服务端实体模型建模

通常,服务端接口的返回值带有一定的业务属性,这些属性代表着特定的业务含义,由这些业务属性组成的数据结构可以代表某类具体的业务实体。我们可以通过 TypeScript 来对这些业务实体进行抽象,举个例子:

现有一接口 /user/detail 会根据请求参数返回某个用户的具体信息,响应数据格式如下:

1
2
3
4
5
6
7
8
9
{
"code": 0,
"error": null,
"data": {
"name": "user name",
"age": 20,
"department": "tech"
}
}
name type description
name string 用户姓名
age number 用户年龄
department string 用户所属部门

/user/detail 返回某个用户的具体信息,包括姓名、年龄和所属部门,根据这些业务属性,可以创建一个针对用户实体模型的抽象接口,后续所有的 IUser 实例都应包含以上三个属性。

1
2
3
4
5
6
7
// 文件: client/types/model.d.ts
// 用户实体接口
interface IUser {
name: string
age: number
department: string
}
  • 请求参数与响应 data 格式

对于某个请求,在定义了服务端响应的数据结构和响应的实体模型后,还需再定义对应的请求参数。还是以 /user/detail 接口为例,它可以根据某个具体的姓名来查找对应的用户信息,可以定义它的请求参数接口为:

1
2
3
4
5
6
7
8
// 文件: client/types/http-api.d.ts
// 获取用户详情请求参数接口
interface IGetUserReq {
name: string
}
//获取用户详情响应 data 接口
type IGetUserRes = IUser | null
  • 异常响应数据

前面我们规定了前后端交互的数据格式,这里我们将定义异常响应数据带来的错误 IHttpError,它在继承前端基础的异常接口 Error 的前提下,额外新增了 namecode 两个属性:

1
2
3
4
5
6
// 文件: client/types/error.d.ts
// http 异常接口
interface IHttpError extends Error {
name: string
code: number
}
name type description
name string IHttpError 的名称
code number 对应响应的 code

在通过 axios 定义请求函数的过程中, IHttpError 类型的错误会通过 Promise.reject(error) 的形式抛出,而全局 window 对象会监听未被处理的 Promise.reject 异常事件 unhandledrejection

1
2
3
4
5
6
7
8
9
10
11
12
13
// 文件: client/common/unhandledrejection.ts
// 监听 unhandledrejection 事件
window.addEventListener('unhandledrejection', function(e: IUnhandledRejection<IHttpError>) {
if (e.detail) {
const { reason } = e.detail
// 当捕获到 `HttpError` 类型的错误时,在页面展示错误信息
if (reason && reason.name === 'HttpError') {
e.preventDefault()
message.error(reason.message)
}
}
})
1
2
3
4
5
6
7
8
9
10
11
// 文件: client/types/event.d.ts
// unhandledrejection 事件 detail 接口
interface IUnhandledRejectionDetail<T> {
promise: Promise<any>
reason: T
}
// unhandledrejection 事件接口
interface IUnhandledRejection<T> extends Event {
detail: IUnhandledRejectionDetail<T>
}
  • axios 的二次封装

这里只对 axios 库进行简单封装,要求:

  • 请求基础 urlhttp://localhost:8080/api
  • 请求头通过 x-requested-with 字段标识是异步请求
  • 如果返回的结果出错,则自动抛出异常

针对 axios 相关的类型定义,可以戳这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 文件: client/services/axios.ts
import axios, {AxiosResponse, AxiosInstance} from 'axios'
// 抛出 http error
function rejectHttpError(message: string, code: string | number = null) {
const error:IHttpError = new Error(message)
error.name = 'HttpError'
if (code != null) {
error.code = code
}
return Promise.reject(error)
}
// 构建 axios 实例
const instance: AxiosInstance = axios.create({
baseURL: 'http://localhost:8080/api/', // 基础请求路径
headers: {
'x-requested-with': 'XMLHttpRequest' // 标志异步请求
}
})
// 自定义预处理返回的响应和错误
instance.interceptors.response.use(
// 预处理正常和异常响应数据
function(response: AxiosResponse<IResponse<any>>) {
let result:IResponse<any> = response.data
// 如果 code 不等于 0 则抛出 http 异常
if (result.code !== 0) {
return rejectHttpError(result.error || '请求异常!', result.code)
}
return response
})
export default instance
  • axios 请求函数

到目前为止,还差最后一步,就是通过 axios 定义某个具体的请求函数,由于异常数据已被我们封装的 axios 内部自动处理,对于请求函数,我们只需关心请求路径、请求参数和响应返回的 data 即可。还是以 /user/detail 接口为例,请求参数类型为 IGetUserReq,响应返回的 data 类型为 IGetUserRes

1
2
3
4
5
6
// 文件: client/services/index.ts
import axios from './axios'
import { AxiosResponse } from 'axios'
// 获取用户详情
export const getUserDetail = (params: IGetUserReq) => axios.get('/user/detail', {params}).then((res: AxiosResponse<IResponse<IGetUserRes>>) => res.data.data)

大部分情况下,对于正常的响应我们只需关心 res.data.data,因此我们可以抽象一个 getResData 函数专门来获取 res.data.data,请求函数定义就变为了

1
2
3
4
5
6
7
8
9
10
11
// 文件: client/services/index.ts
import axios from './axios'
import { AxiosResponse, AxiosPromise } from 'axios'
// 返回 response.data.data
function getResData<T>(promise: AxiosPromise<IResponse<T>>) {
return promise.then((res: AxiosResponse<IResponse<T>>) => res.data.data)
}
// 获取用户详情
export const getUserDetail = (params: IGetUserReq) => getResData<IGetUserRes>(axios.get('/user/detail', {params}))